/*-------------------------------------------------------------------------
 *
 * s_lock.c
 *	   硬件相关的自旋锁实现。
 *
 * 当等待一个竞争的自旋锁时，我们会紧密地循环一段时间，然后使用pg_usleep()延迟并再次尝试。理想情况下，“一段时间”应该是我们期望自旋锁被持有的最大时间的一个小倍数。100次迭代似乎是一个合理的初始猜测。然而，在单处理器上，这种循环是浪费周期的，而在多CPU场景下，通常自旋比调用内核要好，因此我们尝试根据我们似乎是在单处理器还是多处理器中来调整自旋循环计数。
 *
 * 注意：你可能认为MIN_SPINS_PER_DELAY应该是1，但你错了；有些平台可能导致“卡住的自旋锁”故障。尤其是在Alpha上，这种情况已经被观察到；在这种硬件上，从内核空间返回后第一次TAS总是会失败。
 *
 * 一旦我们决定阻塞，我们将使用随机增加的pg_usleep()延迟。第一次延迟为1毫秒，然后延迟随机增加到大约一秒，之后我们重置为1毫秒并重新开始。这样做的想法是在重度竞争的情况下我们需要增加延迟，否则持有自旋锁的线程可能永远无法执行并释放锁。（考虑持有自旋锁的线程优先级被调度器降低的情况——它不会被调度直到所有希望获取锁的线程都在睡眠，因此如果我们总是使用1毫秒的睡眠，实际上存在饥饿的可能性。）但我们不能只是将延迟限制在上限，否则很长时间也不能进行合理的尝试。
 *
 * 我们在NUM_DELAYS延迟后将超时并声明错误（因此，正好这样多次尝试）。在给定的设置下，这通常需要大约2分钟。显然，固定尝试的总数（因此，意外失败的概率）比固定花费的总时间更好。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/storage/lmgr/s_lock.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <time.h>
#include <unistd.h>

#include "common/pg_prng.h"
#include "port/atomics.h"
#include "storage/s_lock.h"

#define MIN_SPINS_PER_DELAY 10
#define MAX_SPINS_PER_DELAY 1000
#define NUM_DELAYS			1000
#define MIN_DELAY_USEC		1000L
#define MAX_DELAY_USEC		1000000L


slock_t		dummy_spinlock;

static int	spins_per_delay = DEFAULT_SPINS_PER_DELAY;


/* 
 * s_lock_stuck() - 抱怨一个卡住的自旋锁
 */
static void fc_s_lock_stuck(const char *fc_file, int fc_line, const char *fc_func)
{
	if (!fc_func)
		fc_func = "(unknown)";
#if defined(S_LOCK_TEST)
	fprintf(stderr,
			"\nStuck spinlock detected at %s, %s:%d.\n",
			fc_func, fc_file, fc_line);
	exit(1);
#else
	elog(PANIC, "stuck spinlock detected at %s, %s:%d",
		 fc_func, fc_file, fc_line);
#endif
}

/*
 * s_lock(lock) - 等待自旋锁的平台无关部分。
 */
int s_lock(volatile slock_t *fc_lock, const char *fc_file, int fc_line, const char *fc_func)
{
	SpinDelayStatus fc_delayStatus;

	init_spin_delay(&fc_delayStatus, fc_file, fc_line, fc_func);

	while (TAS_SPIN(fc_lock))
	{
		perform_spin_delay(&fc_delayStatus);
	}

	finish_spin_delay(&fc_delayStatus);

	return fc_delayStatus.delays;
}

#ifdef USE_DEFAULT_S_UNLOCK
void s_unlock(volatile slock_t *fc_lock)
{
#ifdef TAS_ACTIVE_WORD
	/* HP的PA-RISC */
	*TAS_ACTIVE_WORD(fc_lock) = -1;
#else
	*fc_lock = 0;
#endif
}
#endif

/*
 * 等待在一个争用自旋锁上旋转。
 */
void perform_spin_delay(SpinDelayStatus *fc_status)
{
	/* 每次循环的CPU特定延迟 */
	SPIN_DELAY();

	/* 每经过 spins_per_delay 次尝试阻塞进程 */
	if (++(fc_status->spins) >= spins_per_delay)
	{
		if (++(fc_status->delays) > NUM_DELAYS)
			fc_s_lock_stuck(fc_status->file, fc_status->line, fc_status->func);

		if (fc_status->cur_delay == 0) /* 第一次延迟？ */
			fc_status->cur_delay = MIN_DELAY_USEC;

		pg_usleep(fc_status->cur_delay);

#if defined(S_LOCK_TEST)
		fprintf(stdout, "*");
		fflush(stdout);
#endif

		/* 通过在 1X 和 2X 之间的随机小数增加延迟 */
		fc_status->cur_delay += (int) (fc_status->cur_delay *
									pg_prng_double(&pg_global_prng_state) + 0.5);
		/* 当超过最大值时回到最小延迟 */
		if (fc_status->cur_delay > MAX_DELAY_USEC)
			fc_status->cur_delay = MIN_DELAY_USEC;

		fc_status->spins = 0;
	}
}

/*
 * 在获取自旋锁后，更新关于循环时间的估算。
 *
 * 如果我们能够在没有延迟的情况下获取锁，这表明
 * 我们处于多处理器环境。如果我们不得不延迟，那是一个信号
 * （但不是肯定的）表明我们处于单处理器环境。因此，我们
 * 在需要延迟时慢慢减少 spins_per_delay，而在不需要时迅速增加它。
 * 预计 spins_per_delay 在单处理器上会收敛到最小值，
 * 在多处理器上会收敛到最大值。
 *
 * 注意：spins_per_delay 在我们当前的进程中是本地的。我们希望
 * 在多个后端之间平均这些观察，因为这个函数被调用的情况
 * 相对较少，因此单个后端可能无法存活足够长的时间以收敛到一个
 * 好的值。这由下面的两个例程处理。
 */
void finish_spin_delay(SpinDelayStatus *fc_status)
{
	if (fc_status->cur_delay == 0)
	{
		/* 我们从未需要延迟 */
		if (spins_per_delay < MAX_SPINS_PER_DELAY)
			spins_per_delay = Min(spins_per_delay + 100, MAX_SPINS_PER_DELAY);
	}
	else
	{
		if (spins_per_delay > MIN_SPINS_PER_DELAY)
			spins_per_delay = Max(spins_per_delay - 1, MIN_SPINS_PER_DELAY);
	}
}

/*
 * 在后端启动期间设置 spins_per_delay 的本地副本。
 *
 * 注意：这必须非常快，因为它是在持有自旋锁时调用的
 */
void set_spins_per_delay(int fc_shared_spins_per_delay)
{
	spins_per_delay = fc_shared_spins_per_delay;
}

/*
 * 在后端退出期间更新共享 spins_per_delay 估算。
 *
 * 注意：这必须非常快，因为它是在持有自旋锁时调用的
 */
int update_spins_per_delay(int fc_shared_spins_per_delay)
{
	/*
	 * 我们使用相对较慢的适应率的指数移动平均，
	 * 这样任何一个后端结果中的噪声不会对共享
	 * 值产生太大影响。只要两个输入都在允许的范围内，
	 * 结果也必须如此，因此我们不需要担心限制结果。
	 *
	 * 我们故意使用截断而不是四舍五入；这是为了让
	 * 后端中的单个调整能够影响共享估算（见上面的
	 * 不对称调整规则）。
	 */
	return (fc_shared_spins_per_delay * 15 + spins_per_delay) / 16;
}


/*
 * 各种无法住在 s_lock.h 中的 TAS 实现，因为没有内联
 * 定义（还未）。
 * 将来，消除 tas.[cso] 并将其折叠到此文件中。
 *
 * 如果您在这里更改了一些内容，您可能还需要修改 s_lock.h，
 * 因为这些定义在此文件和 s_lock.h 之间分开。
 */


#ifdef HAVE_SPINLOCKS			/* 如果请求则跳过自旋锁 */


#if defined(__GNUC__)

/*
 * 所有未内联的gcc版本
 */


/*
 * 注意：这里所有的 if 测试可能应该测试 gcc 版本
 * 而不是平台，但我没有足够的信息知道该写什么。
 * 理想情况下，我们会为了内联版本而清除所有这些。
 */
#if defined(__m68k__) && !defined(__linux__)
/* 实际上意味着：extern int tas(slock_t* **lock); */
static void tas_dummy()
{
	__asm__ __volatile__(
#if (defined(__NetBSD__) || defined(__OpenBSD__)) && defined(__ELF__)
/* 标签没有下划线，寄存器没有 % */
						 "\
.global		tas 				\n\
tas:							\n\
			movel	%sp@(0x4),%a0	\n\
			tas 	%a0@		\n\
			beq 	_success	\n\
			moveq	#-128,%d0	\n\
			rts 				\n\
_success:						\n\
			moveq	#0,%d0		\n\
			rts 				\n"
#else
						 "\
.global		_tas				\n\
_tas:							\n\
			movel	sp@(0x4),a0	\n\
			tas 	a0@			\n\
			beq 	_success	\n\
			moveq 	#-128,d0	\n\
			rts					\n\
_success:						\n\
			moveq 	#0,d0		\n\
			rts					\n"
#endif							/* (__NetBSD__ || __OpenBSD__) && __ELF__ */
		);
}
#endif							/* __m68k__ && !__linux__ */
#endif							/* 不是 __GNUC__ */
#endif							/* HAVE_SPINLOCKS */



/*****************************************************************************/
#if defined(S_LOCK_TEST)

/*
 * 用于验证端口自旋锁支持的测试程序。
 */

struct test_lock_struct
{
	char		pad1;
	slock_t		lock;
	char		pad2;
};

volatile struct test_lock_struct test_lock;

int main()
{
	pg_prng_seed(&pg_global_prng_state, (uint64) time(NULL));

	test_lock.pad1 = test_lock.pad2 = 0x44;

	S_INIT_LOCK(&test_lock.lock);

	if (test_lock.pad1 != 0x44 || test_lock.pad2 != 0x44)
	{
		printf("S_LOCK_TEST: failed, declared datatype is wrong size\n");
		return 1;
	}

	if (!S_LOCK_FREE(&test_lock.lock))
	{
		printf("S_LOCK_TEST: failed, lock not initialized\n");
		return 1;
	}

	S_LOCK(&test_lock.lock);

	if (test_lock.pad1 != 0x44 || test_lock.pad2 != 0x44)
	{
		printf("S_LOCK_TEST: failed, declared datatype is wrong size\n");
		return 1;
	}

	if (S_LOCK_FREE(&test_lock.lock))
	{
		printf("S_LOCK_TEST: failed, lock not locked\n");
		return 1;
	}

	S_UNLOCK(&test_lock.lock);

	if (test_lock.pad1 != 0x44 || test_lock.pad2 != 0x44)
	{
		printf("S_LOCK_TEST: failed, declared datatype is wrong size\n");
		return 1;
	}

	if (!S_LOCK_FREE(&test_lock.lock))
	{
		printf("S_LOCK_TEST: failed, lock not unlocked\n");
		return 1;
	}

	S_LOCK(&test_lock.lock);

	if (test_lock.pad1 != 0x44 || test_lock.pad2 != 0x44)
	{
		printf("S_LOCK_TEST: failed, declared datatype is wrong size\n");
		return 1;
	}

	if (S_LOCK_FREE(&test_lock.lock))
	{
		printf("S_LOCK_TEST: failed, lock not re-locked\n");
		return 1;
	}

	printf("S_LOCK_TEST: this will print %d stars and then\n", NUM_DELAYS);
	printf("             exit with a 'stuck spinlock' message\n");
	printf("             if S_LOCK() and TAS() are working.\n");
	fflush(stdout);

	s_lock(&test_lock.lock, __FILE__, __LINE__, PG_FUNCNAME_MACRO);

	printf("S_LOCK_TEST: failed, lock not locked\n");
	return 1;
}

#endif							/* S_LOCK_TEST */
